本系列文章已重新編修,並在加入部分 ES6 新篇章後集結成書,有興趣的朋友可至天瓏書局選購,感謝大家支持。
購書連結 https://www.tenlong.com.tw/products/9789864344130
讓我們再次重新認識 JavaScript!
如果要說 JavaScript 最核心也最容易被誤用的部分,我想應該就非函式 (Function) 莫屬了,那麼在接下來的分享中,我們就來聊聊關於函式的部分。
在前面介紹變數型別的時候曾經說過,除了基本型別以外的都是物件。
當我們透過 typeof
去檢查一個「函式 (function) 」的時候,雖然你會得到 "function"
的結果,讓你以為 function
也是 JavaScript 定義的一種型別,但實際上它仍屬於 Object
的一種。
你可以把它想像成是一種可以被呼叫 (be invoked) 的特殊物件 (值)。
「函式」指的是將一或多段程式指令包裝起來,可以重複使用,也方便維護。
宣告函式的方法有好幾種,但不管是什麼方式,通常一個函式會包含三個部分:
( )
中的部分,稱為「參數 (arguments) 」,參數與參數之間會用逗號 ,
隔開{ }
內的部分,內含需要重複執行的內容,是函式功能的主要區塊。例如:
function square(number) {
return number * number;
}
square(2); // 4
square(3); // 9
square(4); // 16
以上是一個函式的宣告與呼叫簡單示範。
函式使用 function
關鍵字來宣告名稱,參數 number
位於括號之中。
於是透過執行 square(2);
來呼叫 square
函式,此時 square
函式裡面的 number
的值就會是傳進來的 2
,而 number * number
的結果就會是 4
了。
最後再透過 return
回傳結果,如果沒有使用 return
回傳,則預設會回傳 undefined
。
常見定義函式的方式有這幾種:
new Function
關鍵字建立函式下面我們一一介紹。
「函式宣告」應該是屬於最常見的用法:
function 名稱([參數]) {
// 做某事
}
像本篇一開始的範例就是用這種方式:
function square(number) {
return number * number;
}
另一種方式,則是透過 變數名稱 = function([參數]){ ... };
的方式,將一個函式透過 =
指定給某個變數。
像這樣:
var square = function (number) {
return number * number;
};
可能有些人會覺得這樣很奇怪,但還記得我們一直強調的嗎?
函式實際上它仍屬於 Object
的類型,是一種可以被呼叫 (be invoked) 的特殊物件 (值),自然可以透過變數存入囉。
是的,聰明的你也許已經察覺到了,在範例裡 =
後面的 function
是「沒有名字」的:
var square = function (number) {
return number * number;
};
像這類沒有名字的函式在 JavaScript 是合法的,通常我們會稱它為「匿名函式」。
匿名函式我們等等還會見到,現在先介紹到這裡。
在函式運算式中,如果想要在 function
後面加上一個名字是可以的嗎?
可以,像這樣:
var square = function func(number) {
return number * number;
};
但是要注意的是,這個名字只在「自己函式的區塊內」有效,也就是說:
var square = function func(number) {
console.log( typeof func ); // "function"
return number * number;
};
console.log( typeof func ); // undefined
像這樣,脫離了函式自身區塊後,變數 func
就不存在了。
當然,在「匿名函式」的函式運算式情況下,你還是可以透過自定義的變數名稱取得 function
,沒有一定要替這個函式取名的理由:
var square = function func(number) {
console.log( typeof square ); // "function"
return number * number;
};
new Function
關鍵字建立函式最後一種方式就是直接使用 Function
(注意 F
大寫) 這個關鍵字來建立函式物件。 使用時將參數與函式的內容依序傳入 Function
,就可以建立一個函式物件了。 像這樣:
// 透過 new 來建立 Function "物件"
var square = new Function('number', 'return number * number');
透過 new Function
所建立的函式物件,每次執行時都會進行解析「字串」(如 'return number * number'
) 的動作,運作效能較差,所以通常實務上也較少會這樣做。
但不管是透過哪一種方式定義函式,呼叫函式的話就直接用「函式名稱(參數)」的方式,像 square(2);
就可以了。 [註1]
終於要講到全域變數與區域變數的差異了。
在 ES6 之前,JavaScript 變數有效範圍的最小單位是以 function
做分界的。 [註2]
什麼意思呢? 讓我用簡單的範例來說明:
var x = 1;
var doSomeThing = function(y) {
var x = 100;
return x + y;
};
console.log( doSomeThing(50) ); // ?
console.log( x ); // ?
猜猜看,這兩組 console.log()
分別會印出什麼?
.
.
.
答案是 150
與 1
。
由於函式 doSomeThing()
裡面再次定義了變數 x
,所以當我們執行 doSomeThing(50)
時,會將 50
作為參數傳入 doSomeThing()
的 y
,那麼 return x + y
的結果自然就是 100 + 50
的 150
了。
那麼下一行再印出的 x
呢? 為什麼是 1
而不是 100
?
因為...
「切分變數有效範圍的最小單位是 "function" 」
「切分變數有效範圍的最小單位是 "function" 」
「切分變數有效範圍的最小單位是 "function" 」
很重要,所以要講三次。
因為切分變數有效範圍的最小單位是 "function",所以在函式區塊內透過 var
定義的 x
實際上只屬於這個函式。
換句話說,外面的 x
跟 function 內的 x
其實是兩個不同的變數。
因此在最後印出來的 console.log( x );
自然就是外面的 x
也就是 1
了。
所以我們說,變數有效範圍的最小單位是 "function", 這個有效範圍我們通常稱它為「Scope」。
那麼,如果 function 內部沒有 var x
呢?
很簡單,自己的 function 內如果找不到,就會一層層往外找,直到全域變數為止:
var x = 1;
var doSomeThing = function(y) {
// 內部找不到 x 就會到外面找,直到全域變數為止。
// 都沒有就會報錯:ReferenceError: x is not defined
return x + y;
};
console.log( doSomeThing(50) ); // 51
要注意的是, function
可以讀取外層已經宣告的變數,
但外層拿不到裡面宣告的變數。
var
宣告的變數很危險!「沒有 var
宣告的變數很危險」什麼意思?
來,稍微修改一下剛剛的範例,把 function
內的 var
拿掉:
var x = 1;
var doSomeThing = function(y) {
x = 100;
return x + y;
};
console.log( doSomeThing(50) ); // ?
console.log( x ); // ?
猜猜看,這兩組 console.log()
分別會印出什麼?
.
.
.
答案是 ...... 才。不。是。勒~~150
與 1
答案是 150
與 100
。
先別急著崩潰,剛剛說過「切分變數有效範圍的最小單位是 "Function" 」對吧?
但這句話的前提是你得在 function
內部再次用 var
宣告這個變數,否則 JavaScript 會再往外層去找到同名的變數,直到最外層,也就是「全域變數」。
換言之,由於在 function
內沒有重新宣告 x
變數,使得 x = 100
跑去變更了外層的同名變數 x
:
var doSomeThing = function(y) {
x = 100;
return x + y;
};
導致在呼叫 doSomeThing(50)
之後再印出 x
的值自然就變成 100
囉。
覺得混亂了嗎? 還沒完呢。
現在我們把 var
加回去,然後在上面加一行 console.log(x)
像這樣:
var x = 1;
var doSomeThing = function(y) {
console.log(x); // 會出現什麼?
var x = 100;
return x + y;
};
console.log( doSomeThing(50) ); // 150
console.log( x ); // 1
現在我們已經知道 doSomeThing(50)
與 x
的值是 150
以及 1
了,
那麼要讓各位來猜猜看,在 function
內的 console.log(x)
會出現什麼?
.
.
.
答案是 1
或 100
嗎? (打叉)
再猜一次。
.
.
.
正確答案是 undefined
。
醒醒啊,天還沒黑,別急著睡覺。
其實啊,剛剛那份程式碼在瀏覽器 (或者編譯器) 的眼中,是長這樣的:
var x = 1;
var doSomeThing = function(y) {
var x;
console.log(x); // 會出現什麼?
x = 100;
return x + y;
};
console.log( doSomeThing(50) ); // 150
console.log( x ); // 1
看出差異了嗎?
雖然我們這次在函式內部有透過 var
對變數 x
來重新做宣告,但是呢,要是不小心在 var
宣告前就使用了這個變數,這時候 JavaScript
就會開始尋找變數 x
了,在自己的 scope 找... 啊,找到了!
雖然是在下面,但可以確認的是自己的 scope 裡面有宣告,於是就 很貼心地 「只會把宣告的語法」拉到這個 scope 的「最上面」...
(還記得前面介紹變數時講過的嗎? 只要變數有被宣告,使用時就不會有錯誤,否則會出現 ReferenceError
的錯誤。)
最後就變成這個樣子:
var doSomeThing = function(y) {
var x;
console.log(x); // undefined
x = 100;
return x + y;
};
而 JavaScript 的這種特性,我們稱作「變數提升」 (Variables Hoisting)。 [註3]
也因為這種奇怪特性的關係,強烈建議所有可能用到的變數都盡量在 scope 的最上面先宣告完成後再使用。
除了變數以外,函式有沒有提升? 答案是有。
還記得本文一開始說過,函式的定義有分成幾種,其中也可以分成 var xxx = function() {...}
存入變數的「函式運算式」以及直接用 function xxx() {...}
定義的「函式宣告」對吧?
這兩種定義方式最大的差別在於,透過「函式宣告」方式定義的函式可以在宣告前使用 (函式提升) :
square(2); // 4
function square(number) {
return number * number;
}
而透過「函式運算式」定義的函式則是會出現錯誤:
square(2); // TypeError: square is not a function
var square = function (number) {
return number * number;
};
與變數提升的差別在於變數提升只有宣告被提升,而函式的提升則是包括內容完全被提升。 除了可呼叫的時機不同外,「函式宣告」與「函式運算式」在執行時期兩者無明顯差異。
看到這裡,相信你應該對變數的作用範圍有了基本的理解對吧,在本文的最後我再針對「全域變數」與「區域變數」做一些補充說明。
其實在 JavaScript 這門語言中,沒有所謂「全域變數」這種東西。
更準確地說,我們所說的「全域變數」其實指的是「全域物件」(或者叫「頂層物件」) 的屬性。
以瀏覽器來說,「全域物件」指的就是 window
,在 node 環境中則叫做 global
。
舉個例子,我們在最外層透過 var
建立一個變數 a
,像這樣:
var a = 10;
一直以來我們都稱它叫「全域變數」對吧?
這個時候,請你在後面加一行:
var a = 10;
console.log( window.a ); // ?
看到了什麼?
這時你應該會看到剛剛指定給 a
的 10
這個數字才對。
那麼就可以來下個結論:
function
(ES6 的 let
與 const
例外)var
的變數會變成「全域變數」所以看到這裡,相信你應該對「全域變數」與「區域變數」有了更直接的理解吧!
最後分享一下,這是我在網友推特上看到的:
以後有人問你類似問題,相信你也可以抬頭挺胸自信地回答他囉!
來源: https://twitter.com/rayshih771012/status/930075889483726849
[註1] 函式呼叫:除了單純的 函式()
之外,還有 .call()
與 .apply()
,在後續的篇章介紹 this
時會提到這些。
[註2] ES6 之後有 let
與 const
分別定義「變數」與「常數」。 與 var
不同的是,它們的 scope 是透過大括號 { }
來切分的。
[註3] 提升:提升看起來是將變數和函數的宣告移動到程式區塊頂端,但實際上是變數和函數的宣告會在編譯階段中先被放入記憶體,實際在程式碼中位置還是一樣,往上移動的說法是為了幫助理解。
花了好幾天的時間,「重新認識 JavaScript」JS 基礎篇終於告一段落了,各位對 JavaScript 有了基本的理解之後,接著我們要開始進入瀏覽器的部分了。
在接下來的部分都會為各位詳細的介紹。
下一篇:前端工程師的主戰場:瀏覽器裡的 JavaScript,我們明天見。
想請問為什麼 印出的aaa是最後跑完的
because object is call by reference.
var a = new Array();
var b = new Object();
for( let i = 0; i < 5; i++ ){
b.x = i;
console.log( b );
}
and it will print
{ x: 0 }
{ x: 1 }
{ x: 2 }
{ x: 3 }
{ x: 4 }
意思是 迴圈跑完到最後一個"4"
但是aaa每個索引都是引用到這個最後跑完的4 的意思嗎
可是 如果要得到正確的aaa要如何解決?
有試過你另一篇說的 匿名函式包起來的方法 但是不知是我用錯
還是本來就不是這樣用
IIFE 那篇與你的問題是兩件事。
你 push 到 aaa
的物件,從 i = 0
到 i = 4
這五次 push 進去的都是同一個 bbb
物件,每執行一次迴圈, bbb.x
就會被更新成新的數值。
如果你希望每次 push 進去都是新的物件,那麼就在 for 迴圈內建立一個新的物件吧:
var a = new Array();
for( let i = 0; i < 5; i++ ){
let b = new Object();
b.x = i;
a.push( b );
}
console.log(a);
謝謝你
抱歉 這個問題我留錯地方了
Kuro 大大您好
想請教關於函式作用域問題,我改寫ㄧ下您文中的範例
var x = 1;
var doSomeThing = function(y) {
x = 100;
return x + y;
};
console.log( x ); //1
console.log( doSomeThing(50) ); //150
我好奇為何console.log(x),印出來1的結果.
我自己想了一下原因,是否為:JS運作是從上到下解析,因此還未執行到dosomething這個函式,所以函式中未宣告的變數x
,並沒有跑去變更外層的變數x
?
你好,在未呼叫 doSomeThing()
這個 function 以前,裡面的程式碼都不會被執行,所以 x
當然就是原本設定的 1
了。
你可以試試在你提供的程式碼執行的最後再加一行:
var x = 1;
var doSomeThing = function(y) {
x = 100;
return x + y;
};
console.log( x ); // 1
console.log( doSomeThing(50) ); // 150
console.log( x ); // 1 or 100 ?
最後的 x
會是 1 或 100 ?
在 doSomeThing
裡頭的 x = 100;
改成 var x = 100;
後,又會有什麼不同 :)
原來如此,我懂了!謝謝kuro大大
Kuro 老師 你好:
正在研讀您的大作,有些觀念想和您確認一下,所以就來了鐵人賽相同的篇章下發問,
// 若是在全域的狀況下
var a = 10; //使用 var 宣告
b = 10; // 不使用 var
// 是不是都是一樣的宣告方式?因為
console.log(a); // 10
console.log(b); // 10
console.log(window.a); // 10
console.log(window.b); // 10
在 window 下也都能找到 a 與 b 這兩個變數
所以是不是代表在全域的狀況下,有沒有使用 var 宣告變數都是一樣的
而若是在 function 的 {} 內宣告變數,
使用 var 宣告,則可以將變數限制在 {} 內,不會汙染全域,
而若是在 {} 內宣告變數不使用 var 的話,也是會跳出 {} 的範圍汙染到全域,
請問我以上的觀念是否正確,請不吝指正,謝謝
沒錯。 在全域的環境下,不管有沒有透過 var 宣告,這個變數都是全域的。
在 function (){ } 範圍內,若在自己的 scope 沒有宣告變數,則會一層層往上找,直到最外層 (全域) 為止。
a = 1000;
(function (){
var a = 10;
(function (){
a = 20;
console.log(a);
})()
console.log(a);
})()
console.log(a);
像這種情況,雖然內層的 a = 20
沒有 var
但它也不是全域的變數。
那是因為內層的 a = 20 重新賦值外層原本的 var a = 10,這樣對嗎?
我將這段程式放進了 Console,顯示為 20 20 1000
若是最內層的 a = 20 也有 var 的話,
那 Console 就會變成 20 10 1000 了,
這樣是否正確呢?謝謝指教~
是的
謝謝 Kuro 老師
嗚嗚嗚...我真的是JS中文名詞苦手QQ
什麼函式、函數、參數、物件屬性、匿名函式等等等等...搞的頭昏腦花的QQ
好險有Kuro大大寫重新認識JS,謝謝Kuro大大真的幫助我很多... (跪Orz
寫英文也可以啊 XD
var x = 1;
var doSomeThing = function(y) {
var x;
console.log(x); // 會出現什麼?
x = 100;
return x + y;
};
console.log( doSomeThing(50) );
要是不小心在 var 宣告前就使用了這個變數,這時候 JavaScript 就會開始尋找變數 x 了,在自己的 scope 找...
這一段我不太理解想請問「var 宣告前」這句話的意思,因為不是已經有宣告var x = 1;
了,還是說是指 function 裡面的宣告?如果不是指後者的意思的話,不知道我下面的理解是否正確?
我的理解是:當執行到 console.log( doSomeThing(50) );
的時候,JS 會已經從最上方開始找到 var x = 1
,然後再執行 doSomeThing()
。
因為 doSomeThing()
裡面先用到了console.log(x);
然後又再次宣告 x = 100
,所以發生 hoisting. 可是這樣的話,跟您提到的「var 宣告前」好像又沒有關係了@@,因為我認為這樣的情況是因為「再次宣告」才造成 hoisting.
你好, JavaScript var 變數的作用範圍 (scoped) 為 function,所以此範例可以無視外面的 var x = 1;
。
以此範例來說,除非在 function 內沒有加上 var x = 100;
這行,那麼 function 內的 x
與外面的 x
就會是同一個。
var x = 1;
var doSomeThing = function(y) {
// 此即「宣告前」
console.log(x); // 會出現什麼?
// 此處透過 var 宣告 x 變數
var x = 100;
return x + y;
};
console.log( doSomeThing(50) );
想請問~
我新增一個.js檔然後使用vscode做編輯
但是當我直接這樣宣告
a = 10
卻印出a is not defined的error呢?
但是在jsbin上卻正常輸出10。
你好,因為我看不到你的環境,所以無法判斷你的問題出在哪
可以請你附上截圖說明你的問題嗎?
你好,因為 node 環境下不像瀏覽器會將所有未宣告的變數納入 window
這個全域物件下,所以當你在 node 嘗試設定一個尚未宣告的變數 a
就會出現像這樣的訊息。
而在瀏覽器環境下,這個未宣告的變數 a
會自動變成 window
這個全域物件下的屬性,也就是俗稱的全域變數,意義等同於 window.a
。
明白了~
感謝酷囉大大的回覆!!!